package com.cdks.wuxiu.api.controller;

import com.cdks.wuxiu.api.component.annotation.ThirdParty;
import com.cdks.wuxiu.api.component.cdks.kmsg.SingleMessage;
import com.cdks.wuxiu.api.component.constant.CallEnum;
import com.cdks.wuxiu.api.component.constant.Constant;
import com.cdks.wuxiu.api.model.common.AuthParam;
import com.cdks.wuxiu.api.model.dto.CallScoreInfoDTO;
import com.cdks.wuxiu.api.model.dto.UserInfoDTO;
import com.cdks.wuxiu.api.model.dto.WalletInfoDTO;
import com.cdks.wuxiu.api.model.param.CallHangupParam;
import com.cdks.wuxiu.api.model.param.CallCommonParam;
import com.cdks.wuxiu.api.model.param.CallEvaluateParam;
import com.cdks.wuxiu.api.model.param.CallOutParam;
import com.cdks.wuxiu.api.model.param.CallRandParam;
import com.cdks.wuxiu.api.model.param.CallGetScoreListParam;
import com.cdks.wuxiu.api.model.vo.CallAnswerVO;
import com.cdks.wuxiu.api.model.vo.CallCommonVO;
import com.cdks.wuxiu.api.model.vo.CallConfirmVO;
import com.cdks.wuxiu.api.model.vo.CallHangupVO;
import com.cdks.wuxiu.api.model.vo.CallKeepVO;
import com.cdks.wuxiu.api.model.vo.CallOutVO;
import com.cdks.wuxiu.api.model.vo.CallResumeVO;
import com.cdks.wuxiu.api.model.vo.CallSheetVO;
import com.cdks.wuxiu.api.model.vo.UserInfoVO;
import com.cdks.wuxiu.api.model.vo.CallScoreInfoVO;
import com.cdks.wuxiu.api.service.AccountService;
import com.cdks.wuxiu.api.service.AgoraService;
import com.cdks.wuxiu.api.service.AuthService;
import com.cdks.wuxiu.api.service.BookService;
import com.cdks.wuxiu.api.service.CallService;
import com.cdks.wuxiu.api.service.MessageService;
import com.cdks.wuxiu.api.service.UserService;
import com.cdks.wuxiu.api.service.WalletService;
import com.cdks.wuxiu.api.service.redis.RedisService;
import com.cdks.wuxiu.api.util.SmsUtils;
import com.cdks.wuxiu.api.util.TimeUtils;
import com.cdks.wuxiu.common.component.constant.ApiCodeEnum;
import com.cdks.wuxiu.common.exception.ApiException;
import com.cdks.wuxiu.common.model.api.ApiResult;
import com.cdks.wuxiu.entity.message.ChatPrivateData;
import com.cdks.wuxiu.entity.message.CallPrivateData;
import com.cdks.wuxiu.entity.mysql.BookConsultInfo;
import com.cdks.wuxiu.entity.mysql.CallSheetInfo;
import com.cdks.wuxiu.entity.mysql.UserBaseInfo;
import com.cdks.wuxiu.entity.mysql.UserLoginInfo;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.thread.ThreadUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;

import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.io.BufferedReader;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.validation.Valid;


@Api(tags = "通话")
@Slf4j
@RestController
@RequestMapping("/call")
public class CallController {
    @Resource
    private CallService callService;
    @Resource
    private AuthService authService;
    @Resource
    private MessageService messageService; 
    @Resource
    private AccountService accountService;
    @Resource
    private UserService userService;
    @Resource
    private WalletService walletService;
    @Resource
    private BookService bookService;    
    @Resource
    private AgoraService agoraService;
    @Resource
    private RedisService redisService;


    private boolean isAnyCall(int callerType)
    {
        if (callerType > Constant.ACCOUNT_TYPE_TEST)
        {
            return true;
        }

        return false;
    }

    private int checkAnswer(UserBaseInfo caller, UserInfoDTO answer)
    {
        //检查被叫账号状态
        if (answer.getStatus() == Constant.ACCOUNT_STATUS_BANNED)
        {
            throw new ApiException(10, "被叫违规已被封号");
        }

        if (answer.getStatus() == Constant.ACCOUNT_STATUS_DELETED)
        {
            throw new ApiException(10, "被叫已注销账号");
        }

        //检查被叫是否开启勿扰
        if (answer.getIsRefuse() == Constant.ENABLE_YES)
        {
            sendCallHint(caller.getUserId(), answer.getUserId(), caller.getUserId(), 0, Constant.CALL_ENDTYPE_SYSTEM, false);

            throw new ApiException(10, "被叫开启了勿扰，暂时无法接通");
        }

        boolean isBlock  = userService.isBlocked(answer.getUserId(), caller.getUserId());
        if (isBlock)
        {
            throw new ApiException(10, "呼叫失败，你已被对方拉黑");
        }

        return 0;
    }

    private int hangupByCaller(long userId)
    {
        //查找未结束的通话
        CallSheetInfo sheet  = callService.getBusyCallByCaller(userId);
        if (sheet == null)
        {
            return 0;
        }

        //挂断未结束的通话
        sheet.setRemark("系统：" + userId + "发起通话时挂断");
        sheet.setStatus(Constant.CALL_STATUS_STOPPED + Constant.CALL_ENDTYPE_ERROR);
        if (callService.hangup(sheet) == 0)
        {
            //给未结束通话中的另一方发挂断信令
            CallPrivateData data  = new CallPrivateData();
            data  = new CallPrivateData();
            data.setUcid(sheet.getUcid());
            data.setCost(sheet.getPayCost());
            data.setType(sheet.getType());
            data.setWhy(Constant.CALL_ENDTYPE_ERROR);
            data.setHint("通话已结束");
            data.setDuration(sheet.getDuration());
            messageService.sendCallMessage(userId, (userId == sheet.getCallerId()) ? sheet.getAnswerId() : sheet.getCallerId(), Constant.MSG_TYPE_HANGUP, data);
        }

        return 0;
    }

    private int hangupByAnswer(long userId, String ucid)
    {
        CallSheetInfo sheet  = callService.getBusyCallByAnswer(userId);
        if ((sheet == null) || (sheet.getUcid().equals(ucid)))
        {
            return 0;
        }

        sheet.setRemark("系统：" + userId + "接听通话时挂断");
        sheet.setStatus(Constant.CALL_STATUS_STOPPED + Constant.CALL_ENDTYPE_ERROR);
        if (callService.hangup(sheet) == 0)
        {
            //给未结束通话中的另一方发挂断信令
            CallPrivateData data  = new CallPrivateData();
            data  = new CallPrivateData();
            data.setUcid(sheet.getUcid());
            data.setCost(sheet.getPayCost());
            data.setType(sheet.getType());
            data.setWhy(Constant.CALL_ENDTYPE_ERROR);
            data.setHint("通话已结束");
            data.setDuration(sheet.getDuration());
            messageService.sendCallMessage(Constant.USERID_SYSTEM, (userId == sheet.getCallerId()) ? sheet.getAnswerId() : sheet.getCallerId(), Constant.MSG_TYPE_HANGUP, data);
        }

        return 0;
    }

    private int checkBook(long callerId, long answerId, long nowTime)
    {
        //查看主被叫在当前时间是否有在预约时段中并且未结束的服务
        BookConsultInfo order  = bookService.getBusySheetBothUser(callerId, answerId, nowTime);
        if (order == null)
        {
            return 0;
        }

        return bookService.autoSignIn(order, callerId);
    }

    private int sendNextWaitCall(long callee)
    {
        ThreadUtil.execAsync(() -> {
            try {
                //延迟500毫秒
                Thread.sleep(500L);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            
            String ucid  = redisService.getWaitCall(callee);
            if (ucid == null)
            {
                return;
            }

            redisService.removeWaitCall(callee, ucid);

            CallSheetInfo next  = callService.getSheetByUcid(ucid);
            if (next != null)
            {
                //推送通话信令
                CallPrivateData data  = new CallPrivateData();
                data.setUcid(next.getUcid());
                data.setPrice(next.getEarnPrice());
                data.setType(next.getType());

                messageService.sendCallMessage(next.getCallerId(), callee, Constant.MSG_TYPE_CALLOUT, data);
            }
        });

        return 0;
    }

    private int sendCallHint(long sender, long recver[], long caller, long duration, int why, boolean push)
    {
        ChatPrivateData info  = new ChatPrivateData();
        info.setCallerId(caller);
        info.setDuration(duration);
        info.setEndType(why); 
        SingleMessage.sendChatMessage(sender, recver, Constant.MSG_TYPE_CALL, Constant.ENABLE_NO, info); 
        
        if (push)
        {
            //发送离线通知
            messageService.sendOfflineNotice(recver, "未接来电", "您刚才有一次通话未能接听，快去联系一下用户吧~~");
            //为了心诉离线被叫打开APP能显示未接通话消息给被叫发送挂断信令
            CallPrivateData data  = new CallPrivateData();
            data.setUcid(TimeUtils.getLocalDateTime("yyyyMMddHHmmss") + sender);
            data.setCost(0L);
            data.setType(Constant.CALL_TYPE_SINGLE_AUDIO);
            data.setWhy(why);
            data.setHint("通话已结束");
            data.setDuration(0L);
            SingleMessage.sendChatMessage(sender, recver, Constant.MSG_TYPE_HANGUP, Constant.ENABLE_NO, data);
        }

        return 0;
    }

    private int sendCallHint(long sender, long recver, long caller, long duration, int why, boolean push)
    {
        sendCallHint(sender, new long[]{recver}, caller, duration, why, push); 
        
        return 0;
    }

    @ApiOperation("发起呼叫")
    @PostMapping("/callout")
    public ApiResult<Object> callout(@RequestBody @Valid CallOutParam param) {
        if (param.getUserId().equals(param.getDestId()))
        {
            return ApiResult.failed(-10, "被叫信息错误");
        }

        //挂断主叫未结束的通话
        hangupByCaller(param.getUserId());

        UserBaseInfo caller  = param.getUserInfo();
        UserInfoDTO answer   = userService.getUserByIdWithFocus(param.getDestId(), caller.getUserId());
        if (answer == null)
        {
            return ApiResult.failed(10, "呼叫失败，被叫信息错误");
        }

        //官方账号禁止通话呼入
        if ((answer.getType() == Constant.ACCOUNT_TYPE_PLAT) || (answer.getType() == Constant.ACCOUNT_TYPE_KEFU))
        {
            return ApiResult.failed(10, "呼叫失败，对方未开通此项功能");
        }

        //主叫是测试账号检查被叫类型
        if ((caller.getType() == Constant.ACCOUNT_TYPE_TEST) && (answer.getType() != Constant.ACCOUNT_TYPE_TEST))
        {
            return ApiResult.failed(10, "测试账号只能拨打测试账号");
        }

        //检查被叫是否能接通
        checkAnswer(caller, answer);

        int payPrice;
        int earnPrice;
        int minutes;
        int freeNum   = 0;
        long nowTime  = TimeUtils.getTimeSecond();
        //主叫是特殊账号为无限通话，只需要判断对方账号是否异常以及是否在线
        if (isAnyCall(caller.getType()))
        {
            //非同类型账号不能通话
            if ((caller.getType() != answer.getType()) && (caller.getType() != Constant.ACCOUNT_TYPE_KEFU) && (answer.getType() != Constant.ACCOUNT_TYPE_KEFU))
            {
                return ApiResult.failed(10, "呼叫失败，被叫不在线");
            }

            //检查被叫是否在线
            if (answer.getOnlineStatus() == Constant.USER_STATUS_OFFLINE)
            {
                UserLoginInfo info  = userService.getUserLoginInfo(answer.getUserId());
                if (info.getBranch() < 200)
                {
                    return ApiResult.failed(10, "呼叫失败，被叫不在线");
                }
            }

            //检查被叫是否占线
            if (answer.getOnlineStatus() == Constant.USER_STATUS_BUSY)
            {
                return ApiResult.failed(10, "呼叫失败，被叫占线");
            }

            //不计费通话单价为 0，剩余分钟数固定为 10
            payPrice   = 0;
            earnPrice  = 0;
            freeNum    = 0;
            minutes    = 10;
        }
        else
        {
            //查询主叫钱包。这里需要获取有效期大于 60 秒的积分，否则在下次计费之前如果积分过期会造成余额不足
            WalletInfoDTO wallet  = walletService.getUserWalletWithScore(caller.getUserId(), nowTime + Constant.TIME_VALUE_60_SECONDS);
            if (wallet == null)
            {
                return ApiResult.failed(10, "呼叫失败，主叫信息错误");
            }

            //检查是否有未结束的预约咨询服务
            int ret  = checkBook(caller.getUserId(), answer.getUserId(), nowTime);
            if (ret < 0)
            {
                return ApiResult.failed(10, "呼叫失败：您有预约服务待处理");              
            }
    
            Boolean isInBook  = (ret == 0) ? false : true;
            if (isInBook)
            {
                //主被叫在预约的咨询服务时段内，如果主叫余额不足提示充值
                if (wallet.getBean() < answer.getAudioPrice())
                {
                    JSONObject json  = new JSONObject();
                    json.set("scheme", "xinsu://bean/recharge");
                    
                    return ApiResult.failed(11, "余额不足，咨询时段内语音通话需单独付费，请先充值", json);
                }  
            }
            else
            {
                //非同类型账号自动另外分配一个同类型在线用户作为被叫。分配失败时不修改原被叫，不返回失败
                if (caller.getType() != answer.getType())
                {
                    Set<Long> excludes = new HashSet<>();
                    excludes.add(caller.getUserId());

                    do
                    {
                        UserBaseInfo callee  = callService.selectCallee(caller.getUserId(), caller.getType(), caller.getAuthRole(), answer.getSex(), wallet.getBean(), excludes);
                        if (callee == null)
                        {
                            return ApiResult.failed(10, "呼叫失败，被叫不在线");
                        }

                        boolean isBlock  = userService.isBlocked(callee.getUserId(), caller.getUserId());
                        if (isBlock)
                        {
                            continue;
                        }

                        if (callee.getOnlineStatus() == Constant.USER_STATUS_BUSY)
                        {
                            continue;
                        }

                        BeanUtil.copyProperties(callee, answer);
                        break;

                    } while (true);
                }
            }
/*
            //被叫未认证
            if (answer.getAuthRole() == Constant.AUTH_ROLE_TALKER)
            {
                payPrice  = 100;

                if (caller.getAuthRole() > Constant.AUTH_ROLE_TALKER)
                {
                    //主叫已认证并且双方没有接听过的通话记录可以免费通话
                    CallSheetInfo sheet  = callService.getPayedCallBothUser(caller.getUserId(), answer.getUserId());
                    if (sheet == null)
                    {
                        freeNum  = 3;
                    }
                    else
                    {
                        if (wallet.getBean() < payPrice)
                        {
                            JSONObject json  = new JSONObject();
                            json.set("scheme", "xinsu://bean/recharge");
                            
                            return ApiResult.failed(11, "你对该用户的免费分钟数已用完，请先充值", json);
                        }
                    }
                }
            }
            else
            {
                payPrice   = answer.getAudioPrice();
            }

            //发送提示充值弹窗消息
            if ((wallet.getBean() < payPrice) && (freeNum == 0))
            {
                JSONObject json  = new JSONObject();
                json.set("scheme", "xinsu://bean/recharge");
                
                return ApiResult.failed(11, "余额不足。使用语音通话功能请先充值", json);
            }

            earnPrice  = answer.getAudioPrice();
            minutes    = (freeNum > 0) ? freeNum : (wallet.getBean() + wallet.getScore()) / payPrice;
*/
            //被叫未认证
            if (answer.getAuthRole() == Constant.AUTH_ROLE_TALKER)
            {
                payPrice  = Constant.PRICE_CALL_MINAUDIO;
            }
            else
            {
                payPrice   = answer.getAudioPrice();
            }               

            //被叫非咨询师双方没有接听过的通话记录可以免费通话
            if (answer.getAuthRole() != Constant.AUTH_ROLE_CONSULT)
            {
                CallSheetInfo sheet  = callService.getPayedCallBothUser(caller.getUserId(), answer.getUserId());
                if (sheet == null)
                {
                    if (answer.getAuthRole() == Constant.AUTH_ROLE_TALKER)
                    {
                        freeNum  = 3;
                    }
                    else
                    {
                        List<CallSheetInfo> list  = callService.getFreeCallByCaller(caller.getUserId());
                        if (list.size() < Constant.FREE_CALL_NUM)
                        {
                            freeNum  = Constant.FREE_CALL_MINUTE;
                        }
                    }
                }
            }

            if ((wallet.getBean() < payPrice) && (freeNum == 0))
            {
                JSONObject json  = new JSONObject();
                json.set("scheme", "xinsu://bean/recharge");
                
                return ApiResult.failed(11, "与该用户语音通话需要付费，请先充值", json);
            }

            earnPrice  = answer.getAudioPrice();
            minutes    = (freeNum > 0) ? freeNum : (wallet.getBean() + wallet.getScore()) / payPrice;
        }

        //发送管理消息
        if (caller.getType() < Constant.ACCOUNT_TYPE_PLAT)
        {
            SmsUtils.sendManageNotice("用户" + caller.getUserId() + "拨打" + answer.getUserId() + "语音通话");
        }

        //创建话单信息
        CallSheetInfo sheet  = new CallSheetInfo();
        sheet.setCreateTime(nowTime);
        sheet.setCallerId(caller.getUserId());
        sheet.setAnswerId(answer.getUserId());
        sheet.setCallerPlat(param.getBranch());
        sheet.setUcid(TimeUtils.getLocalDateTime("yyyyMMddHHmmss") + caller.getUserId());
        sheet.setType(param.getType());
        sheet.setCallPrice(answer.getAudioPrice());
        sheet.setPayPrice(payPrice);
        sheet.setEarnPrice(earnPrice);
        sheet.setFreeNum(freeNum);

        int ret  = callService.callout(sheet, caller);
        if (ret < 0)
        {
            log.error("call callout failed: " + sheet + ", ret = " + ret);

            return ApiResult.failed(ApiCodeEnum.SERVICE_ERROR);
        }

        if ((caller.getType() == Constant.ACCOUNT_TYPE_KEFU) || (caller.getType() == answer.getType()))
        {
            //只有在线才推送通话信令，避免离线期间收到多次呼叫的问题
            //被叫如果离线，推送离线消息
            if (answer.getOnlineStatus() == Constant.USER_STATUS_OFFLINE)
            {
                //发送sms消息
                if (answer.getType() == Constant.ACCOUNT_TYPE_USER)
                {
                    SmsUtils.sendCallNotice(answer.getPhone(), answer.getBranch());
                }

                //agoraService.sipCallout(sheet.getUcid(), answer.getPhone(), String.valueOf(answer.getUserId()), authService.getCallToken(answer.getUserId(), sheet.getUcid()));
                //发送离线通知
                messageService.sendOfflineNotice(answer.getUserId(), "温馨提示", "您有来电等待接听", "callin.caf");
                //将当前通话加入被叫的待接听通话列表中，用户上线时按时间升序依次获取。否则，如果多个用户同时拨打同一个被叫，直接将呼叫信令发送给被叫会导致被叫登录时弹出多个接听界面
                redisService.saveWaitCall(answer.getUserId(), sheet.getUcid());                
            }
            else
            {
                CallPrivateData data  = new CallPrivateData();
                data.setUcid(sheet.getUcid());
                data.setPrice(sheet.getEarnPrice());
                data.setType(sheet.getType());
                messageService.sendCallMessage(caller.getUserId(), answer.getUserId(), Constant.MSG_TYPE_CALLOUT, data);
            }
        }
        
        //返回结果
        CallOutVO vo  = new CallOutVO();
        vo.setUcid(sheet.getUcid());
        vo.setType(sheet.getType());
        vo.setMinutes(minutes);

        return ApiResult.success(vo);
    }

    @ApiOperation("被叫振铃")
    @PostMapping("/ringing")
    public ApiResult<CallCommonVO> ringing(@RequestBody @Valid CallCommonParam param) {
        CallSheetInfo sheet  = callService.getSheetByUcid(param.getUcid());
        if ((sheet == null) || (sheet.getCallerId().equals(param.getUserId())))
        {
            log.error("call ringing select failed: " + param);

            return ApiResult.failed(10, "通话异常");
        }

        if (sheet.getStatus() == Constant.CALL_STATUS_STOPPED)
        {
            return ApiResult.failed(10, "通话已结束");
        }

        if ((sheet.getStatus() == Constant.CALL_STATUS_STARTED) && (!sheet.getAnswerId().equals(param.getUserId())))
        {
            return ApiResult.failed(10, "已被其他人接听");
        }

        //定向通话才更新话单
        if ((sheet.getType() == Constant.CALL_TYPE_SINGLE_AUDIO) || (sheet.getType() == Constant.CALL_TYPE_SINGLE_VIDEO))
        {
            sheet.setAnswerPlat(param.getBranch());

            //更新话单信息（STATUS_WAITING 状态才会更新成功）
            int ret  = callService.ringing(sheet);
            if (ret == 0)
            {
                //推送通话信令
                CallPrivateData data  = new CallPrivateData();
                data.setUcid(sheet.getUcid());
                data.setPrice(sheet.getPayPrice());
                data.setType(sheet.getType());
                messageService.sendCallMessage(sheet.getAnswerId(), sheet.getCallerId(), Constant.MSG_TYPE_RINGING, data);
            }
        }
        else
        {
            userService.setUserOnlineStatus(param.getUserId(), Constant.USER_STATUS_BUSY);
        }

        //返回结果
        CallCommonVO vo  = new CallCommonVO();
        vo.setUcid(sheet.getUcid());
        vo.setType(sheet.getType());

        return ApiResult.success(vo);
    }

    @ApiOperation("被叫接听")
    @PostMapping("/answer")
    public ApiResult<Object> answer(@RequestBody @Valid CallCommonParam param) {
        CallSheetInfo sheet  = callService.getSheetByUcid(param.getUcid());
        if ((sheet == null) || (sheet.getCallerId().equals(param.getUserId())))
        {
            log.error("call answer select failed: " + param);

            return ApiResult.failed(10, "通话异常", sheet);
        }

        if (sheet.getStatus() >= Constant.CALL_STATUS_STOPPED)
        {
            return ApiResult.failed(10, "通话已结束", sheet);
        }

        if (sheet.getStatus() == Constant.CALL_STATUS_STARTED)
        {
            if (!sheet.getAnswerId().equals(param.getUserId()))
            {
                return ApiResult.failed(10, "已被其他人接听", sheet);
            }
            else
            {
                return ApiResult.failed(-10, "已开始通话", sheet);
            }
        }

        //挂断被叫未结束的通话
        hangupByAnswer(param.getUserId(), param.getUcid());
        //更新用户空闲时间
        userService.setUserIdleTime(param.getUserInfo());

        long nowTime  = TimeUtils.getTimeSecond();
        //一键倾诉更新话单信息
        if ((sheet.getType() == Constant.CALL_TYPE_RANDOM_AUDIO) || (sheet.getType() == Constant.CALL_TYPE_RANDOM_VIDEO))
        {
            UserBaseInfo answer  = param.getUserInfo();
            int freeNum   = 0;
            int earnPrice  = answer.getAudioPrice();
            int payPrice;

            if (answer.getAuthRole() == Constant.AUTH_ROLE_TALKER)
            {
                payPrice  = Constant.PRICE_CALL_MINAUDIO;
            }
            else
            {
                payPrice  = answer.getAudioPrice();
            }

            //被叫非咨询师双方没有接听过的通话记录可以免费通话
            if (answer.getAuthRole() != Constant.AUTH_ROLE_CONSULT)
            {
                //获取双方接听过的通话记录
                CallSheetInfo call  = callService.getPayedCallBothUser(sheet.getCallerId(), answer.getUserId());
                if (call == null)
                {
                    if (answer.getAuthRole() == Constant.AUTH_ROLE_TALKER)
                    {
                        freeNum  = 3;
                    }
                    else
                    {
                        List<CallSheetInfo> list  = callService.getFreeCallByCaller(sheet.getCallerId());
                        if (list.size() < Constant.FREE_CALL_NUM)
                        {
                            freeNum  = Constant.FREE_CALL_MINUTE;
                        }
                    }
                }
            }

            sheet.setAnswerId(answer.getUserId());
            sheet.setCallPrice(answer.getAudioPrice());
            sheet.setPayPrice(payPrice);
            sheet.setEarnPrice(earnPrice);
            sheet.setFreeNum(freeNum);
        }

        //主叫扣费一分钟并且更新话单信息（STATUS_WAITING 状态才会更新成功）
        sheet.setAnswerPlat(param.getBranch());
        sheet.setStartTime(nowTime);
        sheet.setStatus(Constant.CALL_STATUS_STARTED);
        sheet.setPayCost(0L + ((sheet.getFreeNum() > 0) ? 0 : sheet.getPayPrice()));
        sheet.setPayBean(0L + ((sheet.getFreeNum() > 0) ? 0 : sheet.getPayPrice()));

        int ret  = callService.answer(sheet);
        if (ret < 0)
        {
            //接听失败更改为空闲状态
            userService.setUserOnlineStatus(param.getUserId(), Constant.USER_STATUS_BUSY, Constant.USER_STATUS_IDLE);

            //获取下一个待接听通话并发送给用户
            sendNextWaitCall(param.getUserId());

            if (ret == -1)
            {
                sheet  = callService.getSheetByUcid(param.getUcid());
                if ((sheet.getType() == Constant.CALL_TYPE_RANDOM_AUDIO) || (sheet.getType() == Constant.CALL_TYPE_RANDOM_VIDEO))
                {
                    log.info("call answer failed: " + sheet);
                }
                else
                {
                    log.error("call answer failed: " + sheet);
                }

                if (sheet.getStatus() >= Constant.CALL_STATUS_STOPPED)
                {
                    return ApiResult.failed(10, "通话已结束");
                }
        
                if (sheet.getStatus() == Constant.CALL_STATUS_STARTED)
                {
                    if (!sheet.getAnswerId().equals(param.getUserId()))
                    {
                        return ApiResult.failed(10, "已被其他人接听");
                    }
                    else
                    {
                        return ApiResult.failed(-10, "已开始通话");
                    }
                }
                
                return ApiResult.failed(10, "通话异常");
            }
            else
            {
                CallAnswerVO vo  = new CallAnswerVO();
                vo.setUcid(sheet.getUcid());
                vo.setType(sheet.getType()); 
    
                return ApiResult.failed(10, "接听失败，主叫余额不足", vo);
            }
        }
        else
        {
            //一键倾诉有被叫接听需要把该主叫匹配的其他被叫全部挂断
            if ((sheet.getType() == Constant.CALL_TYPE_RANDOM_AUDIO) || (sheet.getType() == Constant.CALL_TYPE_RANDOM_VIDEO))
            {
                callService.hangupRandCall(sheet, Constant.CALL_ENDTYPE_SYSTEM, "已被其他人接听");
            }
        }

        //推送通话信令
        CallPrivateData data  = new CallPrivateData();
        data.setUcid(sheet.getUcid());
        data.setPrice(sheet.getPayPrice());
        data.setType(sheet.getType()); 
        messageService.sendCallMessage(sheet.getAnswerId(), sheet.getCallerId(), Constant.MSG_TYPE_ANSWER, data);

        //开始通话录音
        agoraService.recordStart(sheet.getUcid());

        //返回结果
        CallAnswerVO vo  = new CallAnswerVO();
        vo.setUcid(sheet.getUcid());
        vo.setType(sheet.getType());
        vo.setMinutes(1);
        vo.setCallToken(authService.getCallToken(param.getUserId(), param.getUcid()));

        return ApiResult.success(vo);
    }

    @ApiOperation("主叫确认")
    @PostMapping("/confirm")
    public ApiResult<Object> confirm(@RequestBody @Valid CallCommonParam param) {
        CallSheetInfo sheet  = callService.getSheetByUcidWithCaller(param.getUcid(), param.getUserId());
        if (sheet == null)
        {
            log.error("call confirm failed: " + param);

            return ApiResult.failed(10, "通话异常", sheet);
        }

        //该信令不影响通话的业务逻辑，所以检查话单状态不需要严格同步
        if (sheet.getStatus() != Constant.CALL_STATUS_STARTED)
        {
            log.error("call confirm failed: " + sheet);

            return ApiResult.failed(10, "通话异常", sheet);
        }

        //构造通话信令
        CallPrivateData data  = new CallPrivateData();
        data.setUcid(sheet.getUcid());
        data.setPrice(sheet.getEarnPrice());
        data.setType(sheet.getType());
        messageService.sendCallMessage(sheet.getCallerId(), sheet.getAnswerId(), Constant.MSG_TYPE_CONFIRM, data);

        //返回结果
        CallConfirmVO vo  = new CallConfirmVO();
        vo.setUcid(sheet.getUcid());
        vo.setType(sheet.getType());
        vo.setCallToken(authService.getCallToken(param.getUserId(), sheet.getUcid()));
        //如果是无限通话剩余分钟数固定为 10
        if (isAnyCall(param.getUserInfo().getType()))
        {
            vo.setMinutes(10);
        }
        else
        {
            //这里需要获取有效期大于 60 秒的积分，否则在下次计费之前如果积分过期会造成余额不足
            WalletInfoDTO wallet  = walletService.getUserWalletWithScore(sheet.getCallerId(), TimeUtils.getTimeSecond() + Constant.TIME_VALUE_60_SECONDS);
            int minutes  = (sheet.getFreeNum() > 0) ? sheet.getFreeNum() : (wallet.getBean() + wallet.getScore()) / sheet.getPayPrice();
            vo.setMinutes(minutes);
        }

        return ApiResult.success(vo);
    }

    @ApiOperation("通话挂断")
    @PostMapping("/hangup")
    public ApiResult<CallHangupVO> hangup(@RequestBody @Valid CallHangupParam param) {
        CallSheetInfo sheet  = callService.getSheetByUcid(param.getUcid());
        if (sheet == null)
        {
            log.error("call hangup failed: " + param);

            return ApiResult.failed(10, "通话异常");
        }

        if (sheet.getStatus() >= Constant.CALL_STATUS_STOPPED)
        {
            return ApiResult.failed(10, "通话已结束");
        }

        if ((sheet.getType() == Constant.CALL_TYPE_RANDOM_AUDIO) || (sheet.getType() == Constant.CALL_TYPE_RANDOM_VIDEO))
        {
            //主叫挂断挂断需要把该主叫匹配的其他被叫全部挂断
            //已接听的情况会在hangup里面发送通话报告，所以只需要在被叫拒接时给被叫发送通话报告
            if (sheet.getCallerId().equals(param.getUserId()))
            {
                callService.hangupRandCall(sheet, Constant.CALL_ENDTYPE_CALLER, param.getHint());
            }
            else
            {
                //其他被叫挂断（已接听被叫挂断的情况在answer接口中已处理）
                if (!sheet.getAnswerId().equals(param.getUserId()))
                {
                    if (param.getWhy() != Constant.CALL_ENDTYPE_BUSY)
                    {
                        userService.setUserOnlineStatus(param.getUserId(), Constant.USER_STATUS_IDLE);

                        //被叫未接听需要更新被叫信息
                        if ((sheet.getAnswerId().equals(param.getUserId()) && (param.getWhy() == Constant.CALL_ENDTYPE_CALLEE)) || (param.getWhy() == Constant.CALL_ENDTYPE_TIMEOUT))
                        {
                            callService.updateListenerInfo(sheet, param.getUserInfo());
                        }
                    }

                    //给被叫发送通话报告
                    ChatPrivateData info  = new ChatPrivateData();
                    info.setCallerId(sheet.getCallerId());
                    info.setDuration(sheet.getDuration());
                    info.setEndType(param.getWhy());
                    SingleMessage.sendChatMessage(sheet.getCallerId(), sheet.getAnswerId(), Constant.MSG_TYPE_CALL, Constant.ENABLE_NO, info);

                    redisService.removeRandCall(sheet.getCallerId(), param.getUserId());
                    //如果没有其他匹配被叫，则挂断主叫通话
                    Set<String> ids  = redisService.getRandCall(sheet.getCallerId());
                    if (ids.isEmpty())
                    {
                        CallPrivateData data  = new CallPrivateData();
                        data.setUcid(sheet.getUcid());
                        data.setCost(sheet.getPayCost());
                        data.setType(sheet.getType());
                        data.setWhy(CallEnum.END_BY_SYSTEM.getCode());
                        data.setHint("无人接听"); 
                        data.setDuration(sheet.getDuration()); 

                        messageService.sendCallMessage(Constant.USERID_SYSTEM, sheet.getCallerId(), Constant.MSG_TYPE_HANGUP, data);
                    }

                    //获取下一个待接听通话并发送给用户
                    sendNextWaitCall(param.getUserId());

                    //返回结果
                    CallHangupVO vo  = new CallHangupVO();
                    vo.setUcid(sheet.getUcid());
                    vo.setType(sheet.getType());
                    vo.setDuration(0L);
                    vo.setCost(0L);
                    
                    return ApiResult.success(vo);
                }
            }
        }
        else
        {
            //将当前通话从被叫的待接听列表中删除
            redisService.removeWaitCall(sheet.getAnswerId(), sheet.getUcid());

            if ((!sheet.getAnswerId().equals(param.getUserId())) && (!sheet.getCallerId().equals(param.getUserId())))
            {
                log.error("call hangup failed: " + param);

                return ApiResult.failed(10, "通话异常");
            }
        }

        //更新话单信息并计费
        sheet.setStatus(Constant.CALL_STATUS_STOPPED + param.getWhy());
        sheet.setRemark((sheet.getCallerId().equals(param.getUserId()) ? "主叫：" : "被叫：") + param.getHint());
        int ret  = callService.hangup(sheet);
        //挂断成功，给对方推送挂断信令
        if (ret == 0)
        {
            CallPrivateData data  = new CallPrivateData();
            data.setUcid(sheet.getUcid());
            data.setCost((param.getUserId().equals(sheet.getCallerId())) ? sheet.getEarnCost() : sheet.getPayCost());
            data.setType(sheet.getType());
            data.setWhy(param.getWhy());
            data.setHint(param.getHint());
            data.setDuration(sheet.getDuration());

            messageService.sendCallMessage(param.getUserId(), (param.getUserId().equals(sheet.getCallerId())) ? sheet.getAnswerId() : sheet.getCallerId(), Constant.MSG_TYPE_HANGUP, data);
        }
        
        //停止通话录音
        if (sheet.getDuration() > 0)
        {
            agoraService.recordStop(sheet.getUcid());
        }

        UserBaseInfo caller;
        UserBaseInfo answer;
        if (sheet.getCallerId().equals(param.getUserId()))
        {
            //主叫挂断
            caller  = param.getUserInfo();
            answer  = userService.getUserById(sheet.getAnswerId());
        }
        else
        {
            //被叫挂断
            caller  = userService.getUserById(sheet.getCallerId());
            answer  = param.getUserInfo();
        }

        //定向通话挂断后推送消息（一键倾诉无人接听时 answer = null）
        //同类账号才推送消息（因为同类账号才能互通）
        if ((answer != null) && (caller.getType().equals(answer.getType())))
        {
            switch (param.getWhy())
            {
                case Constant.CALL_ENDTYPE_CALLER:
                {
                    if (caller.getAuthRole() == Constant.AUTH_ROLE_TALKER)
                    {
                        if (sheet.getDuration() == 0)
                        {
                            if (sheet.getType() == Constant.CALL_TYPE_SINGLE_AUDIO)
                            {
                                ChatPrivateData data  = new ChatPrivateData();
                                data.setContent("您拨打的用户没有接听，可以试试随机匹配哦，能够为您智能匹配通话用户");
                                data.setScheme("xinsu://call/random");
                                messageService.sendSystemMessage(caller.getUserId(), Constant.MSG_TYPE_POPUP, data);  
                            }
                        }
                    }

                    break;
                }
                case Constant.CALL_ENDTYPE_CALLEE:
                {
                    if (sheet.getAnswerId().equals(param.getUserId()))
                    {
                        if (answer.getAuthRole() > Constant.AUTH_ROLE_TALKER)
                        {
                            ChatPrivateData data  = new ChatPrivateData();
                            data.setContent("【温馨提示】您刚才拒接了一次通话。如果您不方便接听，为免影响您的接通率，请到“我的”->“通话设置”中打开勿扰模式。");
                            data.setTitle("点击这里立即前往设置");
                            data.setScheme("xinsu://user/callSetting");
                            messageService.sendSystemMessage(answer.getUserId(), Constant.MSG_TYPE_LINK, data);

                            String ucid  = redisService.getWaitCall(answer.getUserId());
                            if (ucid == null)
                            {
                            }
                            else
                            {
                                //获取下一个待接听通话并发送给用户
                                sendNextWaitCall(answer.getUserId());
                            }
                        }
                        else
                        {
                            //获取下一个待接听通话并发送给用户
                            sendNextWaitCall(answer.getUserId());
                        }
                    }

                    break;
                }
                case Constant.CALL_ENDTYPE_TIMEOUT:
                {
                    if (sheet.getDuration() == 0)
                    {
                        if (answer.getAuthRole() > 0)
                        {
                            ChatPrivateData data  = new ChatPrivateData();
                            data.setContent("【温馨提示】您刚才有来电因超时未能接通。如果您不方便接听，为免影响您的接通率，请到“我的”->“通话设置”中打开勿扰模式。");
                            data.setTitle("点击这里立即前往设置");
                            data.setScheme("xinsu://user/callSetting");
                            messageService.sendSystemMessage(answer.getUserId(), Constant.MSG_TYPE_LINK, data);
                        }

                        if (sheet.getType() == Constant.CALL_TYPE_SINGLE_AUDIO)
                        {
                            if ((caller.getAuthRole() == Constant.AUTH_ROLE_CONSULT) || (answer.getAuthRole() == Constant.AUTH_ROLE_CONSULT))
                            {
                            }
                            else
                            {
                                ChatPrivateData data  = new ChatPrivateData();
                                data.setContent("您拨打的用户没有接听，可以试试随机匹配哦，能够为您智能匹配通话用户");
                                data.setScheme("xinsu://call/random");
                                messageService.sendSystemMessage(caller.getUserId(), Constant.MSG_TYPE_POPUP, data);
                            }
                        }                            
                    }

                    break;
                }
                case Constant.CALL_ENDTYPE_BUSY:
                {
                    ChatPrivateData data  = new ChatPrivateData();
                    data.setContent("对方正在通话中，请稍后再拨");
                    messageService.sendSystemMessage(caller.getUserId(), Constant.MSG_TYPE_POPUP, data);
                    
                    break;
                }
            }

            if (sheet.getDuration() > 0)
            {
                if ((answer.getAuthRole() == Constant.AUTH_ROLE_TALKER) && (caller.getAuthRole() == Constant.AUTH_ROLE_TALKER) && (caller.getType() < Constant.ACCOUNT_TYPE_PLAT) && (caller.getSex() == Constant.USER_SEX_FEMALE))
                {
                    ChatPrivateData data  = new ChatPrivateData();

                    data.setContent("【温馨提示】因您未认证聊主刚才通话收入为零。认证聊主后可赚取更多收益，赶快去认证吧~~");
                    data.setTitle("点击这里立即前往认证");
                    data.setScheme("xinsu://user/auth");
                    messageService.sendSystemMessage(answer.getUserId(), Constant.MSG_TYPE_LINK, data);
                }
            }
            else
            {
                if (caller.getAuthRole() == Constant.AUTH_ROLE_TALKER)
                {
                    ThreadUtil.execAsync(() -> {
                        List<UserBaseInfo> userList  = callService.getNoticeCallee(caller.getUserId(), caller.getType(), Constant.AUTH_ROLE_ANSWER, answer.getUserId());
                        if (userList.size() > 0)
                        {
                            int i  = 0;
                            long[] tos = new long[userList.size()];
        
                            for (UserBaseInfo userInfo : userList)
                            {
                                tos[i ++] = userInfo.getUserId();
                            }
        
                            //给其他用户发送未接来电提示
                            sendCallHint(caller.getUserId(), tos, caller.getUserId(), 0, param.getWhy(), true);
                        }
                    });
                }
            }

            if (sheet.getSipId() != null)
            {
                ThreadUtil.execAsync(() -> {
                    agoraService.sipHangup(sheet.getSipId());
                });
            }
        }

/*
        暂时关闭该功能
        if ((sheet.getDuration() > 0) && (caller.getType() == Constant.ACCOUNT_TYPE_KEFU))
        {
            ChatPrivateData data  = new ChatPrivateData();

            data.setContent("邀请您对刚才为您服务的客服进行评价");
            data.setTitle("点击这里立即前往评价");
            data.setScheme("xinsu://web?url=https://xinsu.cdks.net/web/kefu/evaluate.html");
            messageService.sendSystemMessage(sheet.getAnswerId(), Constant.MSG_TYPE_LINK, data);
        }
*/

        //返回结果
        CallHangupVO vo  = new CallHangupVO();
        vo.setUcid(sheet.getUcid());
        vo.setType(sheet.getType());
        vo.setDuration(sheet.getDuration());
        vo.setCost((param.getUserId().equals(sheet.getCallerId())) ? sheet.getPayCost() : sheet.getEarnCost());

        return ApiResult.success(vo);
    }

    @ApiOperation("查看未结束通话")
    @PostMapping("/resume")
    public ApiResult<CallResumeVO> resume(@RequestBody @Valid AuthParam param) {
        CallSheetInfo sheet  = callService.getBusyCallAnyUser(param.getUserId(), param.getUserId());
        if (sheet == null)
        {
            return ApiResult.success();
        }

        //返回结果。这里需要获取有效期大于 60 秒的积分，否则在下次计费之前如果积分过期会造成余额不足
        UserInfoDTO caller  = userService.getUserByIdWithBalance(sheet.getCallerId(), TimeUtils.getTimeSecond() + Constant.TIME_VALUE_60_SECONDS);
        CallResumeVO vo  = new CallResumeVO();
        vo.setUcid(sheet.getUcid());
        vo.setType(sheet.getType());
        vo.setCallToken(authService.getCallToken(param.getUserId(), sheet.getUcid()));
        //无限通话剩余分钟数固定为 10
        vo.setMinutes(isAnyCall(caller.getType()) ? 10 : ((caller.getScore() + caller.getBean()) / sheet.getPayPrice()));

        return ApiResult.success(vo);
    }

    @ApiOperation("通话保持")
    @PostMapping("/keepAlive")
    public ApiResult<CallKeepVO> keepAlive(@RequestBody @Valid CallCommonParam param) {
        CallSheetInfo sheet  = callService.getSheetByUcidWithAnyUser(param.getUcid(), param.getUserId());
        if (sheet == null)
        {
            log.error("call keep failed: " + param);

            return ApiResult.failed(10, "通话异常");
        }
        
        if (sheet.getStatus() == Constant.CALL_STATUS_WAITING)
        {
            return ApiResult.failed(-10, "通话未开始");
        }

        if (sheet.getStatus() >= Constant.CALL_STATUS_STOPPED)
        {
            return ApiResult.failed(10, "通话已结束");
        }

        //更新用户空闲时间
        userService.setUserIdleTime(param.getUserInfo());
        //更新用户通话保持时间
        callService.setCallKeepTime(param.getUserId(), TimeUtils.getTimeSecond(), param.getUcid());

        //实时计费
        int ret  = callService.keepAlive(sheet);
        if (ret < 0)
        {
            //停止通话录音
            agoraService.recordStop(sheet.getUcid());
            //保存账单，被叫计费，并更新用户信息（一键倾诉默认被叫用户ID，因此需要判断被叫）
            if (sheet.getAnswerId() > Constant.USERID_SYSTEM)
            {
                callService.finish(sheet);
            }

            if (ret < -2)
            {
                //主叫当前分钟计费失败，强制挂断通话
                CallPrivateData data  = new CallPrivateData();
                data.setUcid(sheet.getUcid());
                data.setCost(sheet.getEarnCost());
                data.setType(Constant.CALL_TYPE_SINGLE_AUDIO);
                data.setWhy(CallEnum.END_BY_SYSTEM.getCode());
                data.setHint("主叫余额不足"); 
                data.setDuration(sheet.getDuration()); 

                //发送系统挂断消息
                if (param.getUserId().equals(sheet.getCallerId()))
                {
                    messageService.sendCallMessage(Constant.USERID_SYSTEM, sheet.getAnswerId(), Constant.MSG_TYPE_HANGUP, data);

                    //发送提示充值弹窗消息
                    ChatPrivateData info  = new ChatPrivateData();
                    info.setContent("余额不足，若需继续通话请前往充值。");
                    info.setScheme("xinsu://bean/recharge"); 
                    messageService.sendSystemMessage(sheet.getCallerId(), Constant.MSG_TYPE_POPUP, info);

                    return ApiResult.failed(-10, "余额不足");
                }
                else
                {
                    messageService.sendCallMessage(Constant.USERID_SYSTEM, sheet.getCallerId(), Constant.MSG_TYPE_HANGUP, data);

                    //给被叫发送弹窗消息
                    ChatPrivateData info  = new ChatPrivateData();
                    info.setContent("通话结束，主叫余额不足。");
                    messageService.sendSystemMessage(sheet.getCallerId(), Constant.MSG_TYPE_POPUP, info);

                    return ApiResult.failed(-10, "通话结束，主叫余额不足");
                }
            }
            else
            {
                //构造通话信令
                CallPrivateData data  = new CallPrivateData();
                data.setUcid(sheet.getUcid());
                data.setCost(sheet.getEarnCost());
                data.setType(Constant.CALL_TYPE_SINGLE_AUDIO);
                data.setWhy(CallEnum.END_BY_SYSTEM.getCode());
                data.setHint("通话异常"); 
                data.setDuration(sheet.getDuration());

                //发送系统挂断消息
                if (param.getUserId().equals(sheet.getCallerId()))
                {
                    messageService.sendCallMessage(Constant.USERID_SYSTEM, sheet.getAnswerId(), Constant.MSG_TYPE_HANGUP, data);
                }
                else
                {
                    messageService.sendCallMessage(Constant.USERID_SYSTEM, sheet.getCallerId(), Constant.MSG_TYPE_HANGUP, data);
                }

                //发送短信通知
                SmsUtils.sendManageNotice("通话异常：" + sheet.getUcid());

                return ApiResult.failed(10, "通话异常");
            }
        }

        //这里需要获取有效期大于 60 秒的积分，否则在下次计费之前如果积分过期会造成余额不足
        UserInfoDTO caller  = userService.getUserByIdWithBalance(sheet.getCallerId(), TimeUtils.getTimeSecond() + Constant.TIME_VALUE_60_SECONDS);
        CallKeepVO vo  = new CallKeepVO();
        vo.setUcid(sheet.getUcid());
        vo.setType(sheet.getType());
        vo.setDuration(sheet.getDuration());
        //无限通话剩余分钟数固定为 10
        int minutes  = 0;
        if (isAnyCall(caller.getType()))
        {
            minutes  = 10;
        }
        else
        {
            if (sheet.getFreeNum()  > 0)
            {
                long duration  = (sheet.getDuration() + 59) / 60;

                //如果免费分钟数已用完不能再继续通话
                if (sheet.getFreeNum() <= duration)
                {
                    minutes  = 0;
                }
                else
                {
                    minutes  = sheet.getFreeNum() - (int)duration;
                }
            }
            else
            {
                minutes  = (caller.getScore() + caller.getBean()) / sheet.getPayPrice();

                //为了增加留存，付费通话可用分钟数减少一分钟
                if (minutes > 0)
                {
                    minutes  = minutes - 1;
                }
            }
        }

        if (minutes == 0)
        {
            ThreadUtil.execAsync(() -> {
                try {
                    //少500毫秒以防时间误差
                    Thread.sleep((59 - sheet.getDuration() % 60) * 1000 + 500);

                    //停止通话录音
                    agoraService.recordStop(sheet.getUcid());

                    //更新话单信息并计费
                    sheet.setStatus(Constant.CALL_STATUS_STOPPED + Constant.CALL_ENDTYPE_SYSTEM);
                    sheet.setRemark("系统：余额不足");
                    if (callService.hangup(sheet) == 0)
                    {
                        //给主叫发挂断信令
                        CallPrivateData data;
                        data  = new CallPrivateData();
                        data.setUcid(sheet.getUcid());
                        data.setCost(sheet.getPayCost());
                        data.setType(sheet.getType());
                        data.setWhy(Constant.CALL_ENDTYPE_SYSTEM);
                        data.setHint("余额不足");
                        data.setDuration(sheet.getDuration());
                        messageService.sendCallMessage(Constant.USERID_SYSTEM, sheet.getCallerId(), Constant.MSG_TYPE_HANGUP, data);

                        ChatPrivateData info  = new ChatPrivateData();
                        info.setContent("余额不足，若需继续通话请前往充值。");
                        info.setScheme("xinsu://bean/recharge"); 
                        messageService.sendSystemMessage(sheet.getCallerId(), Constant.MSG_TYPE_POPUP, info);

                        //给被叫发挂断信令
                        if (sheet.getSipId() != null)
                        {
                            agoraService.sipHangup(sheet.getSipId());
                        }
                        else
                        {
                            data  = new CallPrivateData();
                            data.setUcid(sheet.getUcid());
                            data.setCost(sheet.getEarnCost());
                            data.setType(sheet.getType());
                            data.setWhy(Constant.CALL_ENDTYPE_SYSTEM);
                            data.setHint("主叫余额不足");
                            data.setDuration(sheet.getDuration());
                            messageService.sendCallMessage(Constant.USERID_SYSTEM, sheet.getAnswerId(), Constant.MSG_TYPE_HANGUP, data);
                        }
                    }
                } catch (InterruptedException e) {
                    log.error("keepAlive error: " + sheet);

                    e.printStackTrace();
                }
            });            
        }
        
        //发送弹窗消息提醒主叫充值
        if (sheet.getCallerId().equals(param.getUserId()) && (minutes < 3))
        {
            ChatPrivateData info  = new ChatPrivateData();
            info.setContent("余额已不足通话" + minutes + "分钟，请立即前往充值");
            info.setScheme("xinsu://bean/recharge");
            messageService.sendSystemMessage(sheet.getCallerId(), Constant.MSG_TYPE_POPUP, info);            
        }

        vo.setMinutes(minutes);

        return ApiResult.success(vo);
    }

    @ApiOperation("查询话单")
    @PostMapping("/getSheet")
    public ApiResult<CallSheetVO> getSheet(@RequestBody @Valid CallCommonParam param) {
        CallSheetInfo sheet  = callService.getSheetByUcid(param.getUcid());
        if (sheet == null)
        {
            log.error("call getSheet failed: " + param);

            return ApiResult.failed(10, "话单信息有误");
        }

        //如果当前用户不是主叫
        if (!sheet.getCallerId().equals(param.getUserId()))
        {
            if ((sheet.getType() == Constant.CALL_TYPE_RANDOM_AUDIO) || (sheet.getType() == Constant.CALL_TYPE_RANDOM_VIDEO))
            {
                if (sheet.getStatus() == Constant.CALL_STATUS_WAITING)
                {
                    //一键倾诉话单未接通时设置被叫为当前用户
                    sheet.setAnswerId(param.getUserId());
                }
                else
                {
                    if (!sheet.getAnswerId().equals(param.getUserId()))
                    {
                        //将当前通话从当前用户的待接听列表中删除
                        redisService.removeWaitCall(param.getUserId(), sheet.getUcid());

                        //获取下一个待接听通话并发送给用户
                        sendNextWaitCall(param.getUserId());

                        return ApiResult.failed(10, "已被其他人接听");
                    }            
                }
            }
            else
            {
                if (!sheet.getAnswerId().equals(param.getUserId()))
                {
                    SmsUtils.sendManageNotice("话单查询用户信息异常");

                    log.error("getSheet failed: param=" + param + ", sheet=" + sheet);

                    return ApiResult.failed(10, "话单异常");
                }
            }
        }

        //通话中话单更新通话时长，客户端在重新进入通话界面时用于计时同步
        if (sheet.getStatus() == Constant.CALL_STATUS_STARTED)
        {
            if (sheet.getStartTime() > 0)
            {
                sheet.setDuration(TimeUtils.getTimeSecond() - sheet.getStartTime());
            }
            else
            {
                sheet.setDuration(TimeUtils.getTimeSecond() - sheet.getCreateTime());
            }
        }

        CallSheetVO vo  = new CallSheetVO();
        BeanUtil.copyProperties(sheet, vo);

        return ApiResult.success(vo);
    }

    @ApiOperation("通话评分")
    @PostMapping("/evaluate")
    public ApiResult<Object> evaluate(@RequestBody @Valid CallEvaluateParam param) {
        CallSheetInfo sheet  = callService.getSheetByUcid(param.getUcid());
        if (sheet == null)
        {
            log.error("call getSheet failed: " + param);

            return ApiResult.failed(10, "话单信息有误，评价失败");
        }

        if (!param.getUserId().equals(sheet.getCallerId()))
        {
            return ApiResult.failed(10, "用户信息有误，评价失败");
        }

        if ((param.getScore() < 3) && (param.getContent() == null))
        {
            return ApiResult.failed(10, "请说明原因以帮助我们提升服务质量");
        }

        if (sheet.getStatus() != Constant.CALL_STATUS_STOPPED)
        {
            //挂断未结束的通话
            sheet.setRemark("系统：通话评价时挂断");
            sheet.setStatus(Constant.CALL_STATUS_STOPPED + Constant.CALL_ENDTYPE_ERROR);
            if (callService.hangup(sheet) == 0)
            {
                //给被叫发挂断信令
                CallPrivateData data  = new CallPrivateData();
                data  = new CallPrivateData();
                data.setUcid(sheet.getUcid());
                data.setCost(sheet.getPayCost());
                data.setType(sheet.getType());
                data.setWhy(Constant.CALL_ENDTYPE_ERROR);
                data.setHint("通话已结束");
                data.setDuration(sheet.getDuration());
                messageService.sendCallMessage(Constant.USERID_SYSTEM, sheet.getAnswerId(), Constant.MSG_TYPE_HANGUP, data);
            }
        }

        int ret  = callService.callEvalute(sheet, param.getScore(), param.getContent());
        if (ret < 0)
        {
            return ApiResult.failed(10, "评价失败");
        }

        if (param.getScore() < 3)
        {
            ChatPrivateData data  = new ChatPrivateData();
            data.setContent("【温馨提醒】用户对您提供的倾诉服务给了一个" + param.getScore() + "分差评，请您认证阅读并执行《倾听者须知》中的内容，努力提高通话服务质量，多次差评将面临平台处罚甚至封禁账号。");
            data.setTitle("点击这里查看《倾听者须知》");
            data.setScheme("xinsu://web?url=https://xinsu.cdks.net/web/protocol/listener.html");
            messageService.sendSystemMessage(sheet.getAnswerId(), Constant.MSG_TYPE_LINK, data);
        }
        else
        {
            ChatPrivateData data  = new ChatPrivateData();
            data.setContent("用户对您提供的倾诉服务给了一个" + param.getScore() + "分评价");
            messageService.sendSystemMessage(sheet.getAnswerId(), Constant.MSG_TYPE_TEXT, data);            
        }

        return ApiResult.success();
    }

    @ApiOperation("获取通话评分列表")
    @PostMapping("/getScoreList")
    public ApiResult<Object> getScoreList(@RequestBody @Valid CallGetScoreListParam param) {
        List<CallScoreInfoDTO> scoreList  = callService.getCallScoreList(param.getPage(), param.getDestId());
        List<CallScoreInfoVO> retVOList  = new ArrayList<>();
        
        for (CallScoreInfoDTO score : scoreList) {
            CallScoreInfoVO vo  = new CallScoreInfoVO();
            vo.userInfo  = new UserInfoVO();
            vo.scoreInfo  = new CallScoreInfoVO.ScoreInfoVO();

            BeanUtil.copyProperties(score, vo.userInfo);
            BeanUtil.copyProperties(score, vo.scoreInfo);

            retVOList.add(vo);
        }

        JSONObject json  = new JSONObject();
        json.set("scoreList", retVOList);

        return ApiResult.success(json);
    }

    @ApiOperation("一键倾诉获取用户列表")
    @PostMapping("/getRandAnswer")
    public ApiResult<Object> getRandAnswer(@RequestBody @Valid AuthParam param) {
        if (param.getUserInfo().getType() == Constant.ACCOUNT_TYPE_KEFU)
        {
            return ApiResult.failed(10, "无权限");
        }

        int sex  = 3 - param.getUserInfo().getSex();
        List<UserBaseInfo> userList  = userService.getRandCallList(param.getUserId(), sex);
        if (userList.size() == 0)
        {
            log.error("一键倾诉列表为空");
        }

        JSONObject json  = new JSONObject();
        json.set("userList", userList);

        return ApiResult.success(json);
    }

    @ApiOperation("一键倾诉发起呼叫")
    @PostMapping("/randCallout")
    public ApiResult<Object> randCallout(@RequestBody @Valid CallRandParam param) {
        UserBaseInfo caller   = param.getUserInfo();
        if (caller.getType() == Constant.ACCOUNT_TYPE_KEFU)
        {
            return ApiResult.failed(10, "未开通此功能");
        }

        //挂断主叫未结束的通话
        long nowTime  = TimeUtils.getTimeSecond();
        hangupByCaller(caller.getUserId());

        //查询主叫钱包。这里需要获取有效期大于 60 秒的积分，否则在下次计费之前如果积分过期会造成余额不足
        WalletInfoDTO wallet  = walletService.getUserWalletWithScore(caller.getUserId(), nowTime + Constant.TIME_VALUE_60_SECONDS);
        if (wallet == null)
        {
            log.error("call randCallout failed: answer is valid, " + caller.getUserId());

            return ApiResult.failed(10, "呼叫失败，主叫信息错误");
        }

        if (param.getCount() == 1)
        {
            //余额小于最低单价并且没有免费通话需要先充值
            if (wallet.getBean() < Constant.PRICE_CALL_MINAUDIO)
            {
                List<CallSheetInfo> list  = callService.getFreeCallByCaller(caller.getUserId());
                if (list.size() >= Constant.FREE_CALL_NUM)
                {
                    JSONObject json  = new JSONObject();
                    json.set("scheme", "xinsu://bean/recharge");
                    
                    return ApiResult.failed(11, "你今日免费通话次数已用完，请先充值", json);                
                }
            }

            //发送管理消息
            SmsUtils.sendManageNotice("用户" + caller.getUserId() + "拨打一键倾诉");
        }

        List<UserBaseInfo> userList  = callService.getRandCallee(caller.getUserId(), caller.getType(), caller.getAuthRole(), 3 - param.getUserInfo().getSex());
        if (userList.size() == 0)
        {
            return ApiResult.failed(-10, "匹配失败，请稍后再试");
        }

        String ucid  = TimeUtils.getLocalDateTime("yyyyMMddHHmmss") + caller.getUserId();
        ArrayList<Long> onlines   = new ArrayList<>();
        ArrayList<Long> offlines  = new ArrayList<>();
        for (UserBaseInfo answer : userList)
        {
            int payPrice;
            if (answer.getAuthRole() == Constant.AUTH_ROLE_TALKER)
            {
                payPrice  = Constant.PRICE_CALL_MINAUDIO;
            }
            else
            {
                payPrice  = answer.getAudioPrice();
            }

            //双方都是咨询师，不匹配
            if ((caller.getAuthRole() == Constant.AUTH_ROLE_CONSULT) && (answer.getAuthRole() == Constant.AUTH_ROLE_CONSULT))
            {
                continue;
            }

            int freeNum  = 0;
            //被叫非咨询师双方没有接听过的通话记录可以免费通话
            if (answer.getAuthRole() != Constant.AUTH_ROLE_CONSULT)
            {
                //获取双方接听过的通话记录
                CallSheetInfo call  = callService.getPayedCallBothUser(caller.getUserId(), answer.getUserId());
                if (call == null)
                {
                    if (answer.getAuthRole() == Constant.AUTH_ROLE_TALKER)
                    {
                        freeNum  = 3;
                    }
                    else
                    {
                        List<CallSheetInfo> list  = callService.getFreeCallByCaller(caller.getUserId());
                        if (list.size() < Constant.FREE_CALL_NUM)
                        {
                            freeNum  = Constant.FREE_CALL_MINUTE;
                        }
                    }
                }
            }

            //余额不足并且没有免费分钟数时不匹配
            if ((wallet.getBean() < payPrice) && (freeNum == 0))
            {
                log.info("randCallout skiped: " + answer.getUserId());

                continue;
            }

            //记录已成功匹配的用户，主叫取消匹配时需要发送挂断信令（离线用户需要依靠挂断信令才能显示通话记录）
            redisService.saveRandCall(caller.getUserId(), answer.getUserId());
            //已匹配到的用户存入已通知列表，在通话挂断时不再推送通知
            redisService.saveNoticedCallee(caller.getUserId(), answer.getUserId());

            if (answer.getOnlineStatus() == Constant.USER_STATUS_OFFLINE)
            {
                offlines.add(answer.getUserId());
                //将匹配到的被叫加入待接听通话列表中，用户上线时按时间升序依次获取。否则，如果多个用户匹配到同一个被叫，直接将呼叫信令发送给被叫会导致被叫登录时弹出多个接听界面
                redisService.saveWaitCall(answer.getUserId(), ucid);
            }
            else
            {
                onlines.add(answer.getUserId());
            }
        }

        if ((offlines.size() + onlines.size()) == 0)
        {
            return ApiResult.failed(-10, "匹配失败，请稍后再试");
        }

        //创建话单信息。通话价格在被叫接听时会更新
        CallSheetInfo sheet  = new CallSheetInfo();
        sheet.setCreateTime(nowTime);
        sheet.setCallerId(caller.getUserId());
        sheet.setAnswerId(param.getSex().longValue());
        sheet.setCallerPlat(param.getBranch());
        sheet.setUcid(ucid);
        sheet.setType(param.getType());
        sheet.setCallPrice(0);
        sheet.setPayPrice(0);
        sheet.setEarnPrice(0);
        sheet.setFreeNum(0);

        int ret  = callService.callout(sheet, caller);
        if (ret < 0)
        {
            log.error("call callout failed: " + sheet + ", ret = " + ret);

            return ApiResult.failed(ApiCodeEnum.SERVICE_ERROR);
        }
        
        if (onlines.size() > 0)
        {
            ThreadUtil.execAsync(() -> {
                long[] tos  = new long[onlines.size()];
                
                for (int i = 0; i < onlines.size(); i++)
                {
                    tos[i] = onlines.get(i);
                }

                //推送通话信令
                CallPrivateData data  = new CallPrivateData();
                data.setUcid(sheet.getUcid());
                data.setPrice(sheet.getEarnPrice());
                data.setType(sheet.getType());
                messageService.sendCallMessage(caller.getUserId(), tos, Constant.MSG_TYPE_CALLOUT, data);
            });
        }

        if (offlines.size() > 0)
        {
            ThreadUtil.execAsync(() -> {
                long[] tos  = new long[offlines.size()];
                
                for (int i = 0; i < offlines.size(); i++)
                {
                    tos[i] = offlines.get(i);
                }

                //发送离线通知
                messageService.sendOfflineNotice(tos, "温馨提示", "您有来电等待接听", "callin.caf");
            });
        }

        log.info("randCallout online: " + onlines);
        log.info("randCallout offline: " + offlines);

        //返回结果
        CallOutVO vo  = new CallOutVO();
        vo.setUcid(sheet.getUcid());
        vo.setType(sheet.getType());
        vo.setMinutes(1);

        return ApiResult.success(vo);
    }

    @ThirdParty
    @ApiOperation("sip回调")
    @RequestMapping("/sipCall")
    public String sipCall(HttpServletRequest request)
    {
        try {
            BufferedReader streamReader  = request.getReader();
            StringBuilder responseStrBuilder  = new StringBuilder();
            String line;
            while ((line = streamReader.readLine()) != null)
            {
                responseStrBuilder.append(line);
            }
            
            JSONObject body = JSONUtil.parseObj(responseStrBuilder.toString());
            log.info("==== sipCall: " + responseStrBuilder.toString());
            if (body.getStr("event") == null)
            {
                return "{\"resCode\":\"0\"}";
            }

            String type  = body.getStr("type");
            if (type == null)
            {
                return "{\"resCode\":\"0\"}";
            }

            if (type.equals("ringing"))
            {
                String callID  = body.getStr("callID");
                CallSheetInfo sheet  = callService.getSheetBySipId(callID);

                if (sheet != null)
                {
                    //更新话单信息（STATUS_WAITING 状态才会更新成功）
                    int ret  = callService.ringing(sheet);
                    if (ret == 0)
                    {
                        //推送通话信令
                        CallPrivateData data  = new CallPrivateData();
                        data.setUcid(sheet.getUcid());
                        data.setPrice(sheet.getPayPrice());
                        data.setType(sheet.getType());
                        messageService.sendCallMessage(sheet.getAnswerId(), sheet.getCallerId(), Constant.MSG_TYPE_RINGING, data);
                    }
                }
                else
                {
                    agoraService.sipHangup(callID);
                }

                return "{\"resCode\":\"0\"}";
            }

            if (type.equals("answer"))
            {
                String callID  = body.getStr("callID");
                CallSheetInfo sheet  = callService.getSheetBySipId(callID);

                if (sheet != null)
                {
                    //更新用户空闲时间
                    userService.setUserIdleTime(sheet.getAnswerId());
                    
                    //检查用户账号状态，异常则强制挂断
                    if (accountService.checkAccount(sheet.getAnswerId()) < 0)
                    {
                        //挂断通话
                        sheet.setRemark("系统：被叫接听，被叫账号异常");
                        sheet.setStatus(Constant.CALL_STATUS_STOPPED + Constant.CALL_ENDTYPE_SYSTEM);
                        if (callService.hangup(sheet) == 0)
                        {
                            agoraService.sipHangup(callID);

                            //构造通话信令
                            CallPrivateData data  = new CallPrivateData();
                            data.setUcid(sheet.getUcid());
                            data.setCost(sheet.getEarnCost());
                            data.setType(Constant.CALL_TYPE_SINGLE_AUDIO);
                            data.setWhy(CallEnum.END_BY_SYSTEM.getCode());
                            data.setHint("被叫账号异常"); 
                            data.setDuration(sheet.getDuration());
                            //发送系统挂断消息
                            messageService.sendCallMessage(Constant.USERID_SYSTEM, sheet.getCallerId(), Constant.MSG_TYPE_HANGUP, data);
                        }

                        return "{\"resCode\":\"0\"}";
                    }

                    //主叫扣费一分钟并且更新话单信息（STATUS_WAITING 状态才会更新成功）
                    int ret  = callService.answer(sheet);
                    if (ret < 0)
                    {
                        log.error("call answer failed: " + sheet + ", ret = " + ret);

                        agoraService.sipHangup(callID);

                        //构造通话信令
                        CallPrivateData data  = new CallPrivateData();
                        data.setUcid(sheet.getUcid());
                        data.setCost(sheet.getEarnCost());
                        data.setType(Constant.CALL_TYPE_SINGLE_AUDIO);
                        data.setWhy(CallEnum.END_BY_SYSTEM.getCode());
                        data.setHint("被叫接听异常"); 
                        data.setDuration(sheet.getDuration());

                        //发送系统挂断消息
                        messageService.sendCallMessage(Constant.USERID_SYSTEM, sheet.getCallerId(), Constant.MSG_TYPE_HANGUP, data);   
                        
                        return "{\"resCode\":\"0\"}";
                    }

                    //推送通话信令
                    CallPrivateData data  = new CallPrivateData();
                    data.setUcid(sheet.getUcid());
                    data.setPrice(sheet.getPayPrice());
                    data.setType(sheet.getType()); 
                    messageService.sendCallMessage(sheet.getAnswerId(), sheet.getCallerId(), Constant.MSG_TYPE_ANSWER, data);

                    //开始通话录音
                    agoraService.recordStart(sheet.getUcid());
                }

                return "{\"resCode\":\"0\"}";
            }

            if (type.equals("hangup"))
            {
                String callID  = body.getStr("callID");
                CallSheetInfo sheet  = callService.getSheetBySipId(callID);

                if (sheet != null)
                {
                    //停止通话录音
                    agoraService.recordStop(sheet.getUcid());

                    //更新话单信息并计费
                    sheet.setStatus(Constant.CALL_STATUS_STOPPED + Constant.CALL_ENDTYPE_CALLEE);
                    sheet.setRemark("被叫：用户挂断");
                    if (callService.hangup(sheet) == 0)
                    {
                        //构造通话信令
                        CallPrivateData data  = new CallPrivateData();
                        data.setUcid(sheet.getUcid());
                        data.setCost(sheet.getPayCost());
                        data.setType(sheet.getType());
                        data.setWhy(Constant.CALL_ENDTYPE_CALLEE);
                        data.setHint("通话已结束");
                        data.setDuration(sheet.getDuration());
                        messageService.sendCallMessage(sheet.getAnswerId(), sheet.getCallerId(), Constant.MSG_TYPE_HANGUP, data);
                    }

                    if (sheet.getDuration() == 0)
                    {
                        sendCallHint(sheet.getCallerId(), sheet.getAnswerId(), sheet.getAnswerId(), 0, Constant.CALL_ENDTYPE_SYSTEM, true);
                    }                    
                }

                return "{\"resCode\":\"0\"}";
            }
        } catch (Exception e) {
            log.error(e.getMessage());
        }

        return "{\"resCode\":\"0\"}";
    }
}
